package com.novunda.tool.qrcode;

import java.util.ArrayList;
import java.util.List;

/**
 * @author linfeng
 * @since 2016/11/10
 */
public class QRCode {

    private final static int PAD0 = 0xEC;
    private final static int PAD1 = 0x11;

    private int typeNumber;
    private int errorCorrectLevel;
    private int moduleCount;
    private Boolean[][] modules;
    private int[] dataCache;
    private List<QR8bitByte> dataList;

    public QRCode(int typeNumber, int errorCorrectLevel) {
        this.typeNumber = typeNumber;
        this.errorCorrectLevel = errorCorrectLevel;
        this.modules = null;
        this.moduleCount = 0;
        this.dataCache = null;
        this.dataList = new ArrayList<QR8bitByte>();
    }

    public void addData(String data) {
        QR8bitByte newData = new QR8bitByte(data);
        this.dataList.add(newData);
        this.dataCache = null;
    }

    public void make() {
        // Calculate automatically typeNumber if provided is < 1
        if (this.typeNumber < 1) {
            int typeNumber;
            for (typeNumber = 1; typeNumber < 40; typeNumber++) {
                List<QRRSBlock> rsBlocks = QRRSBlock.getRSBlocks(typeNumber, this.errorCorrectLevel);

                QRBitBuffer buffer = new QRBitBuffer();
                int totalDataCount = 0;
                for (QRRSBlock rsBlock : rsBlocks) {
                    totalDataCount += rsBlock.getDataCount();
                }

                for (QR8bitByte data : this.dataList) {
                    buffer.put(data.getMode(), 4);
                    buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber));
                    data.write(buffer);
                }
                if (buffer.getLengthInBits() <= totalDataCount * 8)
                    break;
            }
            this.typeNumber = typeNumber;
        }
        this.makeImpl(false, this.getBestMaskPattern());
    }

    public boolean isDark(int row, int col) {
        if (row < 0 || this.moduleCount <= row || col < 0 || this.moduleCount <= col) {
            throw new RuntimeException(row + "," + col);
        }
        return this.modules[row][col];
    }

    public int getModuleCount() {
        return this.moduleCount;
    }

    public void mapData(int[] data, int maskPattern) {

        int inc = -1;
        int row = this.moduleCount - 1;
        int bitIndex = 7;
        int byteIndex = 0;

        for (int col = this.moduleCount - 1; col > 0; col -= 2) {

            if (col == 6) col--;

            while (true) {

                for (int c = 0; c < 2; c++) {

                    if (this.modules[row][col - c] == null) {

                        boolean dark = false;

                        if (byteIndex < data.length) {
                            dark = (((data[byteIndex] >>> bitIndex) & 1) == 1);
                        }

                        boolean mask = QRUtil.getMask(maskPattern, row, col - c);

                        if (mask) {
                            dark = !dark;
                        }

                        this.modules[row][col - c] = dark;
                        bitIndex--;

                        if (bitIndex == -1) {
                            byteIndex++;
                            bitIndex = 7;
                        }
                    }
                }

                row += inc;

                if (row < 0 || this.moduleCount <= row) {
                    row -= inc;
                    inc = -inc;
                    break;
                }
            }
        }
    }

    public void setupPositionProbePattern(int row, int col) {

        for (int r = -1; r <= 7; r++) {

            if (row + r <= -1 || this.moduleCount <= row + r) continue;

            for (int c = -1; c <= 7; c++) {

                if (col + c <= -1 || this.moduleCount <= col + c) continue;

                this.modules[row + r][col + c] = (0 <= r && r <= 6 && (c == 0 || c == 6))
                        || (0 <= c && c <= 6 && (r == 0 || r == 6))
                        || (2 <= r && r <= 4 && 2 <= c && c <= 4);
            }
        }
    }

    public void setupPositionAdjustPattern() {

        int[] pos = QRUtil.getPatternPosition(this.typeNumber);

        for (int row : pos) {

            for (int col : pos) {

                if (this.modules[row][col] != null) {
                    continue;
                }

                for (int r = -2; r <= 2; r++) {

                    for (int c = -2; c <= 2; c++) {

                        this.modules[row + r][col + c] = r == -2 || r == 2 || c == -2 || c == 2
                                || (r == 0 && c == 0);
                    }
                }
            }
        }
    }

    public void setupTimingPattern() {

        for (int r = 8; r < this.moduleCount - 8; r++) {
            if (this.modules[r][6] != null) {
                continue;
            }
            this.modules[r][6] = (r % 2 == 0);
        }

        for (int c = 8; c < this.moduleCount - 8; c++) {
            if (this.modules[6][c] != null) {
                continue;
            }
            this.modules[6][c] = (c % 2 == 0);
        }
    }

    public void setupTypeInfo(boolean test, int maskPattern) {
        int data = (this.errorCorrectLevel << 3) | maskPattern;
        int bits = QRUtil.getBCHTypeInfo(data);

        // vertical
        for (int i = 0; i < 15; i++) {

            boolean mod = (!test && ((bits >> i) & 1) == 1);

            if (i < 6) {
                this.modules[i][8] = mod;
            } else if (i < 8) {
                this.modules[i + 1][8] = mod;
            } else {
                this.modules[this.moduleCount - 15 + i][8] = mod;
            }
        }

        // horizontal
        for (int i = 0; i < 15; i++) {

            boolean mod = (!test && ((bits >> i) & 1) == 1);

            if (i < 8) {
                this.modules[8][this.moduleCount - i - 1] = mod;
            } else if (i < 9) {
                this.modules[8][15 - i - 1 + 1] = mod;
            } else {
                this.modules[8][15 - i - 1] = mod;
            }
        }

        // fixed module
        this.modules[this.moduleCount - 8][8] = (!test);
    }

    public void setupTypeNumber(boolean test) {

        int bits = QRUtil.getBCHTypeNumber(this.typeNumber);

        for (int i = 0; i < 18; i++) {
            boolean mod = (!test && ((bits >> i) & 1) == 1);
            this.modules[Double.valueOf(Math.floor(i / 3)).intValue()][i % 3 + this.moduleCount - 8 - 3] = mod;
        }

        for (int i = 0; i < 18; i++) {
            boolean mod = (!test && ((bits >> i) & 1) == 1);
            this.modules[i % 3 + this.moduleCount - 8 - 3][Double.valueOf(Math.floor(i / 3)).intValue()] = mod;
        }
    }

    public int getBestMaskPattern() {
        int minLostPoint = 0;
        int pattern = 0;

        for (int i = 0; i < 8; i++) {

            this.makeImpl(true, i);

            int lostPoint = QRUtil.getLostPoint(this);

            if (i == 0 || minLostPoint > lostPoint) {
                minLostPoint = lostPoint;
                pattern = i;
            }
        }

        return pattern;
    }

    public void makeImpl(boolean test, int maskPattern) {

        this.moduleCount = this.typeNumber * 4 + 17;
        this.modules = new Boolean[this.moduleCount][];

        for (int row = 0; row < this.moduleCount; row++) {

            this.modules[row] = new Boolean[this.moduleCount];

            for (int col = 0; col < this.moduleCount; col++) {
                this.modules[row][col] = null;//(col + row) % 3;
            }
        }

        this.setupPositionProbePattern(0, 0);
        this.setupPositionProbePattern(this.moduleCount - 7, 0);
        this.setupPositionProbePattern(0, this.moduleCount - 7);
        this.setupPositionAdjustPattern();
        this.setupTimingPattern();
        this.setupTypeInfo(test, maskPattern);

        if (this.typeNumber >= 7) {
            this.setupTypeNumber(test);
        }

        if (this.dataCache == null) {
            this.dataCache = QRCode.createData(this.typeNumber, this.errorCorrectLevel, this.dataList);
        }

        this.mapData(this.dataCache, maskPattern);
    }

    public static int[] createData(int typeNumber, int errorCorrectLevel, List<QR8bitByte> dataList) {
        List<QRRSBlock> rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel);

        QRBitBuffer buffer = new QRBitBuffer();

        for (QR8bitByte data : dataList) {
            buffer.put(data.getMode(), 4);
            buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber));
            data.write(buffer);
        }

        // calc num max data.
        int totalDataCount = 0;
        for (QRRSBlock rsBlock : rsBlocks) {
            totalDataCount += rsBlock.getDataCount();
        }

        if (buffer.getLengthInBits() > totalDataCount * 8) {
            throw new Error("code length overflow. ("
                    + buffer.getLengthInBits()
                    + ">"
                    + totalDataCount * 8
                    + ")");
        }

        // end code
        if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
            buffer.put(0, 4);
        }

        // padding
        while (buffer.getLengthInBits() % 8 != 0) {
            buffer.putBit(false);
        }

        // padding
        while (true) {

            if (buffer.getLengthInBits() >= totalDataCount * 8) {
                break;
            }
            buffer.put(QRCode.PAD0, 8);

            if (buffer.getLengthInBits() >= totalDataCount * 8) {
                break;
            }
            buffer.put(QRCode.PAD1, 8);
        }

        return QRCode.createBytes(buffer, rsBlocks);
    }

    public static int[] createBytes(QRBitBuffer buffer, List<QRRSBlock> rsBlocks) {
        int offset = 0;

        int maxDcCount = 0;
        int maxEcCount = 0;

        int[][] dcdata = new int[rsBlocks.size()][];
        int[][] ecdata = new int[rsBlocks.size()][];

        for (int r = 0; r < rsBlocks.size(); r++) {

            int dcCount = rsBlocks.get(r).getDataCount();
            int ecCount = rsBlocks.get(r).getTotalCount() - dcCount;

            maxDcCount = Math.max(maxDcCount, dcCount);
            maxEcCount = Math.max(maxEcCount, ecCount);

            dcdata[r] = new int[dcCount];

            for (int i = 0; i < dcdata[r].length; i++) {
                dcdata[r][i] = 0xff & buffer.getBuffer().get(i + offset);
            }
            offset += dcCount;

            QRPolynomial rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);
            QRPolynomial rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1);

            QRPolynomial modPoly = rawPoly.mod(rsPoly);
            ecdata[r] = new int[rsPoly.getLength() - 1];
            for (int i = 0; i < ecdata[r].length; i++) {
                int modIndex = i + modPoly.getLength() - ecdata[r].length;
                ecdata[r][i] = (modIndex >= 0) ? modPoly.get(modIndex) : 0;
            }

        }

        int totalCodeCount = 0;
        for (QRRSBlock rsBlock : rsBlocks) {
            totalCodeCount += rsBlock.getTotalCount();
        }

        int[] data = new int[totalCodeCount];
        int index = 0;

        for (int i = 0; i < maxDcCount; i++) {
            for (int r = 0; r < rsBlocks.size(); r++) {
                if (i < dcdata[r].length) {
                    data[index++] = dcdata[r][i];
                }
            }
        }

        for (int i = 0; i < maxEcCount; i++) {
            for (int r = 0; r < rsBlocks.size(); r++) {
                if (i < ecdata[r].length) {
                    data[index++] = ecdata[r][i];
                }
            }
        }

        return data;
    }

}
